package com.googlecode.mvcguice.routing;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.InvocationHandler;

import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ListMultimap;
import com.googlecode.mvcguice.action.Actions;
import com.googlecode.mvcguice.util.ReflectionUtils;

public final class ReverseRouter {
	private final ListMultimap<Action, UrlGenerator> reverseRoutes;

	private ReverseRouter(Builder builder) {
		this.reverseRoutes = builder.reverseRoutes;
	}

	public String forAction(Class<?> controller, String method, Object... args) {
		return forAction(controller, lookupMethod(controller, method), args);
	}

	private final Map<Class<?>, Object> cachedProxies = new ConcurrentHashMap<Class<?>, Object>();

	public final <T> T redirect(final Class<T> controller) {
		@SuppressWarnings("unchecked")
		final T cachedInstance = (T) cachedProxies.get(controller);
		if (cachedInstance != null) {
			return cachedInstance;
		}

		final Enhancer enhancer = new Enhancer();
		enhancer.setUseFactory(true);
		enhancer.setSuperclass(controller);
		enhancer.setCallback(new InvocationHandler() {
			@Override
			public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
				if (ReflectionUtils.isObjectMethod(method)) {
					return null;
				}

				return Actions.redirect(forAction(controller, method, args));
			}
		});

		@SuppressWarnings("unchecked")
		final T instance = (T) enhancer.create();
		cachedProxies.put(controller, instance);
		return instance;
	}

	private String forAction(Class<?> controller, Method method, Object... args) {
		final Action action = new Action(controller, method);
		final List<UrlGenerator> generators = reverseRoutes.get(action);
		for (UrlGenerator generator : generators) {
			final String url = generator.generate(args);
			if (url != null) {
				return url;
			}
		}

		String msg = "Unable to find reverse route for action: " + action + " with args: " + Arrays.toString(args);
		// TODO: replace it
		throw new RuntimeException(msg);
	}

	private static Method lookupMethod(Class<?> controller, String methodName) {
		for (Method method : controller.getMethods()) {
			if (method.getName().equals(methodName)) {
				return method;
			}
		}

		throw new IllegalArgumentException("Method: '" + methodName + "' in controller: '" + controller.getName()
				+ "' is not found");
	}

	public static class Builder {
		final ListMultimap<Action, UrlGenerator> reverseRoutes = ArrayListMultimap.create();

		public void addRule(Class<?> controller, Method method, UrlGenerator urlGenerator) {
			Action action = new Action(controller, method);
			reverseRoutes.put(action, urlGenerator);
		}

		public ReverseRouter build() {
			return new ReverseRouter(this);
		}
	}

	private static class Action {
		private final Class<?> controller;
		private final Method method;

		public Action(Class<?> controller, Method method) {
			this.controller = controller;
			this.method = method;
		}

		@Override
		public int hashCode() {
			return controller.hashCode() + 31 * method.hashCode();
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj) {
				return true;
			}
			if (obj == null) {
				return false;
			}
			if (getClass() != obj.getClass()) {
				return false;
			}

			Action other = (Action) obj;
			if (!controller.equals(other.controller)) {
				return false;
			}
			if (!method.equals(other.method)) {
				return false;
			}
			return true;
		}

		@Override
		public String toString() {
			return controller.getName() + "." + method.getName() + Arrays.toString(method.getParameterAnnotations());
		}
	}
}
